using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading;

namespace IllSoft.Xvcc.Volano.Client
{
    public class VolanoClient : IVolanoClient
    {
        #region Members

        private AsyncOperation _asyncOperation;

        private byte[] _serverMessageBuffer = new byte[1];

        #endregion Members

        public VolanoClient()
        {
            this.Channels = new Channels();
        }

        private void ConnectCallback(IAsyncResult result)
        {
            _asyncOperation = AsyncOperationManager.CreateOperation(null);

            this.ConnectionManager.EndConnect(result);

            this.ConnectionManager.BufferedStream.BeginRead(
                this._serverMessageBuffer,
                0,
                this._serverMessageBuffer.Length,
                new AsyncCallback(this.ReceiveCallback),
                null);

            this.SendAuthentication();
        }

        private void ReceiveCallback(IAsyncResult ar)
        {
            if (this.ConnectionManager.BufferedStream.EndRead(ar) == 0)
            {
                Trace.WriteLine("Disconnected!");
                return;
            }

            _asyncOperation.Post(new SendOrPostCallback(delegate
                {
                    this.ProcessPacket();
                }), null);
        }

        #region IVolanoClient Members

        public event EventHandler Authenticated;

        public event EventHandler Authenticating;

        public event EventHandler<ChannelListEventArgs> ChannelListReceived;

        public event EventHandler Connected;

        public event EventHandler Connecting;

        public event EventHandler Disconnected;

        public event EventHandler Disconnecting;

        public event EventHandler<MessageEventArgs> Message;

        public event EventHandler PacketReceived;

        public event EventHandler PacketReceiving;

        public event EventHandler PingReceived;

        public event EventHandler PongSending;

        public event EventHandler PongSent;

        public event EventHandler UnknownPacketReceived;

        public event EventHandler<UserListEventArgs> UserListReceived;

        public Channels Channels { get; private set; }

        public Queries Queries { get; private set; }

        internal ConnectionManager ConnectionManager { get; private set; }

        public ConnectionArgs ConnectionArgs
        {
            get { return this.ConnectionManager.ConnectionArgs; }
        }

        public void Connect(ConnectionArgs connectionArgs)
        {
            this.ConnectionManager = new ConnectionManager(connectionArgs);

            this.ConnectionManager.BeginConnect(new AsyncCallback(this.ConnectCallback));
        }

        public Channel CreateChannel(string name, User myself)
        {
            Channel channel = new Channel(name, myself, this);

            this.Channels.Add(channel.Name, channel);

            return channel;
        }

        public Query CreateQuery(string text, User myself)
        {
            Query query = new Query();

            this.Queries.Add(text, query);

            return query;
        }

        public void Disconnect()
        {
            //this.ConnectionManager.Disconnect();
        }

        public void ProcessAudioAlarm()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(4);

            string channelName = this.ConnectionManager.VolanoReader.ReadString();
            string otherNick = this.ConnectionManager.VolanoReader.ReadString();
            string myNick = this.ConnectionManager.VolanoReader.ReadString();

            Channel channel = new Channel(channelName, this);
            User otherUser = new User(otherNick);

            this.Channels[channelName].OnAudioAlarm(
                new AudioAlarmEventArgs(
                    this.Channels[channelName],
                    otherUser,
                    EventDetails.Someone));
        }

        public void ProcessAuthentication()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(11);

            KeyGen keyGen = new KeyGen();
            byte[] command = ClientCommand.ChannelList;
            byte[] challenge = this.ConnectionManager.VolanoReader.ReadBytes(this.ConnectionManager.VolanoReader.ReadLength());
            byte[] response = keyGen.GenerateSignedKey(challenge);

            this.ConnectionManager.VolanoWriter.Write(command,
                new byte[] { (byte)response.Length },
                response);

            this.ConnectionManager.VolanoWriter.Flush();
        }

        public void ProcessChannelJoin()
        {
            int numberOfUsers;
            string channelName;
            string myNick;
            string myProfile;
            string otherNick;
            string otherProfile;
            Users users = new Users();

            this.ConnectionManager.VolanoReader.ReadBytes(3);

            switch (this.ConnectionManager.VolanoReader.ReadByte())
            {
                case 2: // Someone else joined
                    otherProfile = this.ConnectionManager.VolanoReader.ReadString();
                    channelName = this.ConnectionManager.VolanoReader.ReadString();
                    otherNick = this.ConnectionManager.VolanoReader.ReadString();

                    // Dummy reads but neccessary
                    this.ConnectionManager.VolanoReader.ReadString();
                    this.ConnectionManager.VolanoReader.ReadString();
                    this.ConnectionManager.VolanoReader.ReadString();

                    this.Channels[channelName].Users.Add(new User(otherNick, otherProfile));

                    this.Channels[channelName].OnJoined(new ChannelEventArgs(
                        this.Channels[channelName],
                        new User(otherNick, otherProfile),
                        EventDetails.Someone,
                        JoinError.Success
                        ));

                    return;

                case 4: // I joined
                    myProfile = this.ConnectionManager.VolanoReader.ReadString();
                    channelName = this.ConnectionManager.VolanoReader.ReadString();
                    myNick = this.ConnectionManager.VolanoReader.ReadString();
                    this.ConnectionManager.VolanoReader.ReadBytes(3);

                    switch (this.ConnectionManager.VolanoReader.ReadByte())
                    {
                        case 1: // Channel joined successfully, save user list
                            this.ConnectionManager.VolanoReader.ReadBytes(6);
                            numberOfUsers = this.ConnectionManager.VolanoReader.ReadLength();
                            this.ConnectionManager.VolanoReader.ReadBytes(4);

                            for (int i = 0; i < numberOfUsers; i++)
                            {
                                otherNick = this.ConnectionManager.VolanoReader.ReadString();
                                otherProfile = this.ConnectionManager.VolanoReader.ReadString();
                                users.Add(new User(otherNick, otherProfile));

                                // Dummy reads but neccessary
                                this.ConnectionManager.VolanoReader.ReadString();
                                this.ConnectionManager.VolanoReader.ReadString();
                                this.ConnectionManager.VolanoReader.ReadString();
                            }

                            this.Channels[channelName].Users = users;

                            // Here we need to raise two events:
                            // 1. Successfully joined the channel
                            this.Channels[channelName].OnJoined(new ChannelEventArgs(
                                this.Channels[channelName],
                                new User(myNick, myProfile),
                                EventDetails.Myself,
                                JoinError.Success
                                ));

                            // 2. Current user list of the joined channel
                            this.Channels[channelName].OnUserListReceived(new UserListEventArgs(
                                this.Channels[channelName]
                                ));

                            return;

                        case 2: // Channel is full
                            this.ConnectionManager.VolanoReader.ReadBytes(12);

                            this.Channels[channelName].OnJoined(new ChannelEventArgs(
                                this.Channels[channelName],
                                new User(myNick, myProfile),
                                EventDetails.Myself,
                                JoinError.ChannelFull
                                ));

                            return;

                        case 3: // Nick is already in use
                            this.ConnectionManager.VolanoReader.ReadBytes(12);

                            this.Channels[channelName].OnJoined(new ChannelEventArgs(
                                this.Channels[channelName],
                                new User(myNick, myProfile),
                                EventDetails.Myself,
                                JoinError.NickInUse
                                ));

                            return;

                        case 4: // Nick is already taken by a member/contains non-allowed characters
                            this.ConnectionManager.VolanoReader.ReadBytes(12);

                            this.Channels[channelName].OnJoined(new ChannelEventArgs(
                                this.Channels[channelName],
                                new User(myNick, myProfile),
                                EventDetails.Myself,
                                JoinError.NickRegistered
                                ));

                            return;
                    }
                    return;
            }
        }

        public void ProcessChannelList()
        {
            int numberOfChannels;
            string channelName;
            StringCollection channels;

            this.ConnectionManager.VolanoReader.ReadBytes(10);
            numberOfChannels = this.ConnectionManager.VolanoReader.ReadLength();
            channels = new StringCollection();

            for (int i = 0; i < numberOfChannels; i++)
            {
                channelName = this.ConnectionManager.VolanoReader.ReadString();
                channels.Add(channelName);
            }

            // Prepare event args and raise event
            this.OnChannelListReceived(new ChannelListEventArgs(channels));
        }

        public void ProcessChannelPart()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(3);

            User user;
            string channelName;
            byte partType;

            partType = this.ConnectionManager.VolanoReader.ReadByte();
            channelName = this.ConnectionManager.VolanoReader.ReadString();
            user = new User(this.ConnectionManager.VolanoReader.ReadString());

            User partedUser = (from u in this.Channels[channelName].Users
                              where u.Nick == user.Nick
                              select u).Single();

            this.Channels[channelName].Users.Remove(partedUser);

            switch (partType)
            {
                case 1: // I have left the channel
                    this.Channels[channelName].OnParted(new PartEventArgs(user, EventDetails.Myself));

                    // Synchronize channels
                    this.Channels.Remove(channelName);

                    break;

                case 2: // Someone else has left the channel
                    this.Channels[channelName].OnParted(new PartEventArgs(user, EventDetails.Someone));

                    break;
            }
        }

        public void ProcessMessage()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(3);

            string channelName;
            string nick;
            string message;
            string handle;

            switch (this.ConnectionManager.VolanoReader.ReadByte())
            {
                case 2: // Message from someone else
                    this.ConnectionManager.VolanoReader.ReadBytes(2);
                    handle = this.ConnectionManager.VolanoReader.ReadHandle();
                    User otherUser;

                    // Convert.ToBase64("") == "AAAA" ... not nice but it works ...
                    // A handle of "AAAA" means that there is no handle
                    if (handle == "AAAA")
                    {
                        // Channel message
                        channelName = this.ConnectionManager.VolanoReader.ReadString();
                        nick = this.ConnectionManager.VolanoReader.ReadString();
                        message = this.ConnectionManager.VolanoReader.ReadString();
                        otherUser = new User(nick);

                        this.Channels[channelName].OnMessage(new MessageEventArgs(otherUser, message, EventDetails.Someone));

                    }
                    /*else
                    {
                        // Query message
                        this.ConnectionManager.VolanoReader.ReadBytes(2);
                        nick = this.ConnectionManager.VolanoReader.ReadString();
                        message = this.ConnectionManager.VolanoReader.ReadString();
                        otherUser = new User(nick, string.Empty, handle);

                        Trace.WriteLine(string.Format("<'{0}'> {1}", otherUser, message));
                        this.queries[handle].RaiseMessageEvent(
                            new MessageEventArgs(otherUser, message, EventDetails.Someone));
                    }*/

                    return;

                case 4: // My message was accepted
                    this.ConnectionManager.VolanoReader.ReadBytes(5);
                    channelName = this.ConnectionManager.VolanoReader.ReadString();
                    this.ConnectionManager.VolanoReader.ReadBytes(4);

                    // Prepare event args and raise event
                    this.Channels[channelName].OnMessage(new MessageEventArgs(null, null, EventDetails.Myself));
                    //this.OnChannelMessage(
                    //    new MessageEventArgs(channelName, null, null, MessageType.ISaid));

                    return;
            }
        }

        private void ProcessPacket()
        {
            byte serverMessage = 0;
            bool knownServerMessage = true;

            serverMessage = this.ConnectionManager.VolanoReader.ReadByte();

            ServerCommand serverCommand = (ServerCommand)(Enum.Parse(typeof(ServerCommand), serverMessage.ToString()));
            Trace.WriteLine("[" + DateTime.Now.ToString() + "] ServerCommand Message: " + Convert.ToString(serverMessage) + " - " + serverCommand.ToString());

            switch (serverCommand)
            {
                case ServerCommand.AudioAlarm:
                    this.ProcessAudioAlarm();
                    break;

                case ServerCommand.Authentication:
                    this.ProcessAuthentication();
                    break;

                case ServerCommand.ChannelJoin:
                    this.ProcessChannelJoin();
                    break;

                case ServerCommand.ChannelList:
                    this.ProcessChannelList();
                    break;

                case ServerCommand.ChannelPart:
                    this.ProcessChannelPart();
                    break;

                case ServerCommand.Message:
                    this.ProcessMessage();
                    break;

                case ServerCommand.Ping:
                    this.ProcessPing();
                    break;

                case ServerCommand.QueryClose:
                    this.ProcessQueryClose();
                    break;

                case ServerCommand.QueryRequest:
                    this.ProcessQueryRequest();
                    break;

                case ServerCommand.UserList:
                    this.ProcessUserList();
                    break;

                default:
                    knownServerMessage = false;
                    break;
            }

            if (knownServerMessage)
            {
                //TODO: Add event
            }
            else
            {
                //this.ProcessUnknownPacket();
            }

            Trace.WriteLine("Continueing to read data...");
            this.ConnectionManager.BufferedStream.BeginRead(
                this._serverMessageBuffer,
                0,
                this._serverMessageBuffer.Length,
                new AsyncCallback(this.ReceiveCallback),
                null);
        }

        public void ProcessPing()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(4);

            // Prepare event args and raise event
            //this.OnPingReceived(EventArgs.Empty);

            // Answer to the ping request
            this.SendPong();
        }

        public void ProcessQueryClose()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(5);

            string handle = this.ConnectionManager.VolanoReader.ReadHandle();
            string otherNick = this.ConnectionManager.VolanoReader.ReadString();
            User otherUser = new User(otherNick, string.Empty);

            //this.queries[handle].RaiseLeftEvent(
            //    new QueryEventArgs(this.queries[handle].Myself, otherUser));
        }

        public void ProcessQueryRequest()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(3);

            string channelName = string.Empty;
            string handle = string.Empty;
            string myNick = string.Empty;
            string otherNick = string.Empty;
            string otherProfile = string.Empty;
            User myself;
            User otherUser;

            switch (this.ConnectionManager.VolanoReader.ReadByte())
            {
                case 2: // We received a query request
                    channelName = this.ConnectionManager.VolanoReader.ReadString();
                    otherNick = this.ConnectionManager.VolanoReader.ReadString();
                    myNick = this.ConnectionManager.VolanoReader.ReadString();
                    this.ConnectionManager.VolanoReader.ReadBytes(1);
                    handle = this.ConnectionManager.VolanoReader.ReadHandle();
                    this.ConnectionManager.VolanoReader.ReadBytes(2);
                    otherProfile = this.ConnectionManager.VolanoReader.ReadString();

                    myself = new User(myNick);
                    otherUser = new User(otherNick, otherProfile, handle);

                    // Prepare event args and raise event
                    this.Channels[channelName].OnQuery(new QueryEventArgs(myself, otherUser));

                    return;

                case 4: // Our query request was accepted
                    channelName = this.ConnectionManager.VolanoReader.ReadString();
                    myNick = this.ConnectionManager.VolanoReader.ReadString();
                    otherNick = this.ConnectionManager.VolanoReader.ReadString();
                    this.ConnectionManager.VolanoReader.ReadBytes(1);
                    handle = this.ConnectionManager.VolanoReader.ReadHandle();
                    this.ConnectionManager.VolanoReader.ReadBytes(4);

                    myself = new User(myNick);
                    otherUser = new User(otherNick, otherProfile, handle);

                    // Prepare event args and raise event
                    //this.OnQueryRequestSentAccepted(
                    //    new QueryEventArgs(myself, otherUser));

                    return;
            }
        }

        public void ProcessUnknownPacket()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void ProcessUserList()
        {
            this.ConnectionManager.VolanoReader.ReadBytes(4);

            int numberOfUsers;
            string channelName;
            string nick;
            string profile;
            Channel channel;
            Users users = new Users();

            channelName = this.ConnectionManager.VolanoReader.ReadString();

            this.ConnectionManager.VolanoReader.ReadBytes(12);

            numberOfUsers = this.ConnectionManager.VolanoReader.ReadLength();
            for (int i = 0; i < numberOfUsers; i++)
            {
                nick = this.ConnectionManager.VolanoReader.ReadString();
                profile = this.ConnectionManager.VolanoReader.ReadString();
                users.Add(new User(nick, profile));

                // Dummy reads but neccessary
                this.ConnectionManager.VolanoReader.ReadString();
                this.ConnectionManager.VolanoReader.ReadString();
                this.ConnectionManager.VolanoReader.ReadString();
            }

            // Prepare event args and raise event
            if (this.Channels.ContainsKey(channelName))
            {
                channel = this.Channels[channelName];
            }
            else
            {
                channel = new Channel(channelName, this);
                channel.Users = users;
            }

            this.OnUserListReceived(new UserListEventArgs(channel));
        }

        protected virtual void OnAuthenticated(EventArgs e)
        {
            EventHandler handler = this.Authenticated;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnAuthenticating(EventArgs e)
        {
            EventHandler handler = this.Authenticating;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnChannelListReceived(ChannelListEventArgs e)
        {
            EventHandler<ChannelListEventArgs> handler = this.ChannelListReceived;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnDisconnected(EventArgs e)
        {
            EventHandler handler = this.Disconnected;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnMessage(MessageEventArgs e)
        {
            EventHandler<MessageEventArgs> handler = this.Message;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnPingReceived(EventArgs e)
        {
            EventHandler handler = this.PingReceived;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnPongSending(EventArgs e)
        {
            EventHandler handler = this.PongSending;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnPongSent(EventArgs e)
        {
            EventHandler handler = this.PongSent;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        private void OnUserListReceived(UserListEventArgs e)
        {
            EventHandler<UserListEventArgs> handler = this.UserListReceived;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        public void Reconnect()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void SendAudioAlarm(Channel channel, User myself, User otherUser)
        {
            this.ConnectionManager.VolanoWriter.Write(ClientCommand.AudioAlarm);
            this.ConnectionManager.VolanoWriter.Write(
                channel.Name,
                myself.Nick,
                otherUser.Nick);

            // Send the server message
            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            /*this.OnAudioAlarm(
                new AudioAlarmEventArgs(channel, myself, otherUser, EventDetails.Myself));*/
        }

        public void SendAuthentication()
        {
            /*
            // Prepare event args and raise event
            this.OnAuthenticating(new EventArgs());
            */

            this.ConnectionManager.VolanoWriter.Write(ClientCommand.Authentication);
            this.ConnectionManager.VolanoWriter.Write(
                this.ConnectionManager.ConnectionArgs.AppletVersion,
                this.ConnectionManager.ConnectionArgs.AppletUrl,
                this.ConnectionManager.ConnectionArgs.ServerUrl,
                this.ConnectionManager.ConnectionArgs.JvmVendor,
                this.ConnectionManager.ConnectionArgs.JvmVendorUrl,
                this.ConnectionManager.ConnectionArgs.JvmVersion,
                this.ConnectionManager.ConnectionArgs.JvmClassVersion,
                this.ConnectionManager.ConnectionArgs.Platform,
                this.ConnectionManager.ConnectionArgs.WindowsVersion,
                this.ConnectionManager.ConnectionArgs.Architecture);

            // Send the server message
            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            this.OnAuthenticated(EventArgs.Empty);
        }

        public void SendChannelJoin(Channel channel, User myself)
        {
            this.ConnectionManager.VolanoWriter.Write(ClientCommand.ChannelJoin);
            this.ConnectionManager.VolanoWriter.Write(
                myself.Profile,
                channel.Name,
                myself.Nick);

            // Send the server message
            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            /*this.channels[channelName].RaiseJoinedEvent(
                JoinType.JoinRequestSent,
                myself
                );
            //new ChannelJoinEventArgs(channelName, JoinType.JoinRequestSent, myself));
            */
        }

        public void SendChannelPart(Channel channel, User myself)
        {
            this.ConnectionManager.VolanoWriter.Write(ClientCommand.ChannelPart);
            this.ConnectionManager.VolanoWriter.Write(
                channel.Name,
                myself.Nick);

            // Send the server message
            this.ConnectionManager.VolanoWriter.Flush();

            this.Channels.Remove(channel.Name);

            // Prepare event args and raise event
            /*this.OnChannelLeft(
                new ChannelLeaveEventArgs(channel, myself.Nick, LeaveType.LeaveRequestSent));
            */
        }

        public void SendGetUserList(string channelName)
        {
            this.ConnectionManager.VolanoWriter.Write(ClientCommand.UserList);
            this.ConnectionManager.VolanoWriter.Write(channelName);

            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            // TODO: Add event
        }

        public void SendMessage(Channel channel, User myself, string message)
        {
            this.ConnectionManager.VolanoWriter.Write(
                ClientCommand.ChannelMessage,
                Convert.FromBase64String(string.Empty));
            // TODO: Change this: Remove Handler property or what?
            /*this.ConnectionManager.VolanoWriter.Write(ClientCommand.ChannelMessage,
                Convert.FromBase64String(channel.Handle));
            */
            this.ConnectionManager.VolanoWriter.Write(
                channel.Name,
                myself.Nick,
                message);

            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            //this.OnChannelMessage(
            //    new MessageEventArgs(myself, message, MessageType.MessageSent));
            //channel.RaiseMessageEvent(
        }

        public void SendMessage(Query query, User myself, string message)
        {
            foreach (User user in query.Users)
            {
                this.ConnectionManager.VolanoWriter.Write(
                    ClientCommand.QueryMessage,
                    Convert.FromBase64String(user.Handle));
                this.ConnectionManager.VolanoWriter.Write(
                    string.Empty,
                    myself.Nick,
                    message);
            }

            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            //this.OnChannelMessage(
            //    new MessageEventArgs(myself, message, MessageType.MessageSent));
            //channel.RaiseMessageEvent(
        }

        public void SendPong()
        {
            // Send the pong reply
            this.ConnectionManager.VolanoWriter.Write(ClientCommand.Pong);

            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            //this.OnPongSent(new EventArgs());
        }

        public void SendQueryClose(User myself, User otherUser)
        {
            this.ConnectionManager.VolanoWriter.Write(
                ClientCommand.QueryClose,
                Convert.FromBase64String(otherUser.Handle));
            this.ConnectionManager.VolanoWriter.Write(myself.Nick);

            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            /*this.OnQueryCloseReceived(
                new QueryEventArgs(myself, otherUser));
            */
        }

        public void SendQueryRequest(Channel channel, User myself, User otherUser)
        {
            this.ConnectionManager.VolanoWriter.Write(ClientCommand.QueryRequest);
            this.ConnectionManager.VolanoWriter.Write(
                channel.Name,
                myself.Nick,
                otherUser.Nick);

            this.ConnectionManager.VolanoWriter.Flush();

            // Prepare event args and raise event
            /*
            this.OnQueryRequestSent(
                new QueryEventArgs(myself, otherUser));
            */
        }

        #endregion
    }
}